﻿using System;
using System.Data;
using System.Text;

namespace BizElements.Core
{
    /// <summary>
    /// Part of a predicate. Used internally by predicate classes.
    /// </summary>
    [Serializable]
    public sealed class PredicateItem : ISqlItem, IRenderSql
    {
        // Contained SQL items implement rendering, thus IPredicateItemRenderer or a similar interface is not needed.
        // Internally, a lot of other non-predicate classes use PredicateItem as renderable ISqlItem implementations.
        // Eg: AggregateFunctionFactory, InsertExpression, OracleInserter.

        #region Members.

        /// <summary>
        /// Item: field, subquery, function or free text.
        /// </summary>
        object item;

        /// <summary>
        /// Item type.
        /// </summary>
        SqlItemType itemType;

        /// <summary>
        /// SQL data type.
        /// </summary>
        DbType dbType = DbType.Object;

        /// <summary>
        /// .NET type of data stored in the column.
        /// </summary>
        Type dataType = typeof(object);

        /// <summary>
        /// Alias or name. May be <b>null</b>.
        /// </summary>
        string alias;

        #endregion

        #region Constructors and initializers.

        /// <summary>
        /// Creates a new predicate item.
        /// </summary>
        /// <param name="item">Item.</param>
        /// <param name="itemType">Item type.</param>
        internal PredicateItem(object item, SqlItemType itemType)
        {
            switch (itemType)
            {
                case SqlItemType.Column:
                    InitColumn((IDbColumn)item);
                    break;
                case SqlItemType.FreeText:
                    InitFreeText((string)item);
                    break;
                case SqlItemType.Function:
                    InitFunction((Function)item);
                    break;
                case SqlItemType.Parameter:
                    DbParameter par = (DbParameter)item;
                    InitParameter(par.Value, par.DbType, par.Size, par.ParameterName);
                    break;
                case SqlItemType.SubQuery:
                    InitQuery((SelectStatement)item);
                    break;
            }
        }

        /// <summary>
        /// Creates a new item from the given column.
        /// </summary>
        internal PredicateItem(IDbColumn column)
        {
            InitColumn(column);
        }

        private void InitColumn(IDbColumn column)
        {
            this.item = column;
            this.itemType = SqlItemType.Column;
            this.dataType = column.DataType;
            this.dbType = column.DbType;
            this.alias = column.Alias;
        }

        /// <summary>
        /// Creates a new item from the given text expression.
        /// </summary>
        private PredicateItem(string freeText)
        {
            InitFreeText(freeText);
        }

        private void InitFreeText(string freeText)
        {
            this.item = freeText;
            this.itemType = SqlItemType.FreeText;
        }

        /// <summary>
        /// Creates a new item from the given function.
        /// </summary>
        internal PredicateItem(Function function)
        {
            InitFunction(function);
        }

        private void InitFunction(Function function)
        {
            this.item = function;
            this.itemType = SqlItemType.Function;
            this.dataType = function.DataType;
            this.dbType = function.DbType;
            this.alias = function.Alias;
        }

        /// <summary>
        /// Creates a new parameter and an item which holds it.
        /// </summary>
        /// <param name="parameterValue">Value.</param>
        /// <param name="parameterType">SQL parameter type.</param>
        /// <param name="size">Size of the parameter. Required only for variable length types.</param>
        /// <param name="parameterNamePrefix">Parameter name prefix (eg. "par"). A unique parameter ID will
        /// be automatically appended to the prefix during the rendering phase if required.</param>
        public PredicateItem(object parameterValue, DbType parameterType, int size, string parameterNamePrefix)
        {
            InitParameter(parameterValue, parameterType, size, parameterNamePrefix);
        }

        private void InitParameter(object parameterValue, DbType parameterType, int size, string parameterNamePrefix)
        {
            // Unique parameter ID is automatically appended to parameter name during the rendering phase.
            // Eg: 'parameter_15'. This is a safety precaution. It is possible that there are more
            // parameters with the same name.

            DbParameter dbpar = new DbParameter();
            dbpar.ParameterName = SqlItemUtil.CleanParameterName(parameterNamePrefix);
            dbpar.Direction = ParameterDirection.Input;
            dbpar.DbType = parameterType;
            dbpar.Value = parameterValue;
            if (size > 0)
                dbpar.Size = size;

            this.item = dbpar;
            this.itemType = SqlItemType.Parameter;
            this.dataType = TypeUtil.NetTypeFrom(parameterType);
            this.dbType = parameterType;
        }

        /// <summary>
        /// Creates a new item from the given query.
        /// </summary>
        internal PredicateItem(SelectStatement query)
        {
            InitQuery(query);
        }

        private void InitQuery(SelectStatement query)
        {
            this.item = query;
            this.itemType = SqlItemType.SubQuery;
            SqlItemUtil.TryDetermineSubQueryDataType(query, out this.dataType, out this.dbType);
            this.alias = query.Alias;
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets contained item.
        /// </summary>
        public object Item
        {
            get { return this.item; }
        }

        /// <summary>
        /// Gets item type.
        /// </summary>
        public SqlItemType ItemType
        {
            get { return this.itemType; }
        }

        /// <summary>
        /// Gets the SQL data type. DbType.Object if unknown.
        /// </summary>
        public DbType DbType
        {
            get { return this.dbType; }
        }

        /// <summary>
        /// Gets .NET type. Object if unknown.
        /// </summary>
        public Type DataType
        {
            get { return this.dataType; }
        }

        /// <summary>
        /// Gets the item's alias/name. May be <b>null</b>.
        /// </summary>
        public string Alias
        {
            get { return this.alias; }
        }

        #endregion

        #region Methods.

        /// <summary>
        /// Renders predicate item as SQL statement.
        /// </summary>
        /// <param name="dbms">Target DBMS.</param>
        /// <param name="output">StringBuilder to which SQL is appended.</param>
        /// <param name="parameters">SQL parameter collection to which the object's and its children's
        /// parameters are added. After the rendering is done the collection contains all parameters with unique names.</param>
        public void Render(DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            switch (this.itemType)
            {
                case SqlItemType.Column:
                    IDbColumn col = (IDbColumn)this.item;
                    col.RenderFullName(dbms, output);
                    break;

                case SqlItemType.Parameter:
                    DbParameter par = (DbParameter)this.item;
                    par.Render(dbms, output, parameters);
                    break;

                case SqlItemType.SubQuery:
                    SelectStatement select = (SelectStatement)this.item;
                    select.Render(dbms, output, parameters);
                    break;

                case SqlItemType.FreeText:
                    output.Append((string)this.item);
                    break;

                case SqlItemType.Function:
                    Function func = (Function)this.item;
                    func.Render(dbms, output, parameters);
                    break;

                default:
                    throw new InvalidOperationException(Messages.PredicateItem_UnsupportedItemType + this.itemType.ToString());
            }
        }

        #endregion
    }	
}